package uk.co.q3c.deplan.client.domain.resource;

/**
 * A representation of a resource used to fulfil a {@link Task}. In addition to basic resource information (a name!), this class also
 * contains sets of calendar adjustments and a profile. The calendar adjustments are changes to this redource's availability and are
 * combined with {@link DefaultCalendarAdjustment} to provide input to the profile. The profile records availability per day (how much time
 * this resource has available) and utilisation, (how much of that available time has been used by tasks). Time left = available - utilised.
 * 
 * @see ResourceGroup
 * @author DSowerby 23 Apr 2008
 */
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import uk.co.q3c.deplan.client.domain.DomainKind;
import uk.co.q3c.deplan.client.domain.task.BaseTask;
import uk.co.q3c.deplan.client.domain.task.StartFinishTimes;
import uk.co.q3c.deplan.client.domain.util.BlankCreator;
import uk.co.q3c.deplan.client.service.ServiceFactory;
import uk.co.q3c.deplan.client.util.Q3DateUtil;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Cached;
import com.googlecode.objectify.annotation.Serialized;
import com.googlecode.objectify.annotation.Unindexed;

@Unindexed
@Cached(expirationSeconds = 600)
public class IndividualResource extends AbstractResource implements Serializable, BlankCreator<ResourceProfileEntry> {
	private static Logger logger = Logger.getLogger(IndividualResource.class.getName());
	private static final long serialVersionUID = 1L;
	@Serialized
	private ResourceDailyProfile_LinkedList bookingProfile;
	@Serialized
	private ResourceCalendar resourceCalendar;

	private Key<IndividualResource> key;

	protected IndividualResource() {
		super();
	}

	public IndividualResource(ServiceFactory connection) {
		super(connection);
	}

	/**
	 * 
	 * @see uk.co.q3c.deplan.client.domain.resource.domain.resource.Resource#firstProfileDate()
	 */
	public String firstProfileDate() {
		return bookingProfile.first()
				.getDateKey();
	}

	public ResourceCalendar getResourceCalendar() {

		return resourceCalendar;
	}

	@Override
	public String toString() {

		return getName();
	}

	/**
	 * Returns the amount of effort available up to but not greater than the specified level of units on the specified date. This equates to
	 * the baseWorkingTime adjusted by holidays and the like, and then reduced by the amount of time already utilised. A call with di==null
	 * will return 0. If there is insufficient effort available to meet the <code>units</code> specified, the maximum available effort is
	 * returned
	 * 
	 * @see uk.co.q3c.deplan.client.domain.resource.domain.resource.Resource#effortAvailable(uk.co.q3c.deplan.util.DateIndex, double)
	 */
	public int effortAvailable(Date date, double units) {
		if (date == null) {
			logger.warning("date is null in call to effortAvailable");
			return 0;
		}

		// get the profile entry, a default one will be created if needed
		ResourceProfileEntry rpe = bookingProfile.getEntry(date, true);
		logger.fine("Entry for " + date + " has utilised time of " + rpe.getUtilisedTime());
		int requestedEffort = (int) (resourceCalendar.getWorkingTime(date) * units);

		int availableEffort = resourceCalendar.getWorkingTime(date) - rpe.getUtilisedTime();

		if (availableEffort > requestedEffort) {
			return requestedEffort;
		}
		return availableEffort;
	}

	@SuppressWarnings("deprecation")
	public StartFinishTimes bookEffort(int effortWanted, Date date, BaseTask task) throws PersistenceReferenceException {

		if (effortWanted == 0) {
			return null;
		}
		// second parameter true so a new entry for the date is created
		// automatically if none exists
		ResourceProfileEntry rpe = bookingProfile.getEntry(date, true);

		StartFinishTimes sft = new StartFinishTimes();
		// set the start as first available minute
		sft.start = startTime(rpe, date);
		rpe.allocateWork(effortWanted);
		// plannedFinish time is start + work - 1
		Date d = new Date(sft.start);
		d.setMinutes(d.getMinutes() + effortWanted - 1);
		sft.finish = d.getTime();
		logger.info("booked " + effortWanted + " minutes of resource: " + getName() + " on " + new Date(sft.start).toLocaleString());
		task.bookEffort(this, date, effortWanted);
		return sft;
	}

	/**
	 * Calculates start time for a booking
	 */
	@SuppressWarnings("deprecation")
	private long startTime(ResourceProfileEntry rpe, Date date) {
		Date d = Q3DateUtil.startOfDay_date(date);
		d.setMinutes(d.getMinutes() + rpe.getUtilisedTime());
		// cal.add(Calendar.MINUTE, rpe.getUtilisedTime());
		return d.getTime();
	}

	/**
	 * Each individual resource has its own {@link ResourceCalendar}, and this is combined with a baseCalendar to provide a composite global
	 * and individual calendar adjustments.
	 */
	public void setBaseCalendar(BaseCalendar baseCalendar) {
		resourceCalendar.setBaseCalendar(baseCalendar);
	}

	public BaseCalendar getBaseCalendar() {
		return resourceCalendar.getBaseCalendar();
	}

	/**
	 * returns the number of minutes booked on the specified day, 0 if date out of range
	 * 
	 * @param date
	 *            The date to look up
	 * @return
	 */
	public int timeBooked(Date date) {

		ResourceProfileEntry rpe = bookingProfile.getEntry(date);
		if (rpe == null) {
			return 0;
		} else {
			return rpe.getUtilisedTime();
		}
	}

	/**
	 * Add an adjustment to working time for a given day. For a resource this is applied after any adjustments made at the base level (which
	 * apply to all resources). Note that there is no <code>removeAdjustment</code>, just call this with <code>absolute==true</code> and
	 * <code>minutes==0</code>
	 * 
	 * @param date
	 *            the date affected. Time is ignored. String must be a valid date for use with DateFormat
	 * 
	 * @param absolute
	 *            if true adjustment is absolute, else it is relative to the baseCalendar
	 * 
	 * @param minutes
	 *            number of minutes by which to adjust (positive or negative)
	 */
	// @Override
	public void addAdjustment(Date date, boolean absolute, int minutes) {

		getResourceCalendar().addAdjustment(date, absolute, minutes);

	}

	@Override
	public List<IndividualResource> getResources() {
		List<IndividualResource> r = new ArrayList<IndividualResource>();
		r.add(this);
		return r;
	}

	@Override
	public boolean isGroup() {
		return false;
	}

	// @Override
	/**
	 * Returns 'this' in a list
	 */
	public List<IndividualResource> resources() {
		ArrayList<IndividualResource> list = new ArrayList<IndividualResource>();
		list.add(this);
		return list;
	}

	@Override
	/**
	 * Returns 'this' in the provided list
	 */
	public List<IndividualResource> getIndividualResources(List<IndividualResource> list) {
		list.add(this);
		return list;
	}

	@Override
	public int count() {
		return 1;
	}

	@Override
	public boolean uses(IndividualResource usedResource) {
		return this == usedResource;
	}

	/**
	 * <code>cal</code> isn't used, as the entry doesn't hold the date
	 * 
	 * @see BlankCreator#blank(Date)
	 */
	@Override
	public ResourceProfileEntry blank(Date date) {
		ResourceProfileEntry rpe = new ResourceProfileEntry();
		return rpe;
	}

	public int getTotalEffortBooked() {
		return bookingProfile.totalUtilisedTime();
	}

	@Override
	protected void beforeLoad() {
		super.beforeLoad();

	}

	public void clearBookings() {
		bookingProfile.clear();
	}

	public int getWorkingTime(Date date) {

		return resourceCalendar.getWorkingTime(date);
	}

	@Override
	public void setConnection(ServiceFactory connection) {
		super.setConnection(connection);
		resourceCalendar.setConnection(connection);

	}

	@Override
	public void afterLoad(ServiceFactory connection) {
		super.afterLoad(connection);
		if (resourceCalendar == null) {
			resourceCalendar = new ResourceCalendar(connection);
			if (bookingProfile == null) {
				bookingProfile = new ResourceDailyProfile_LinkedList();
			}

		}
		resourceCalendar.afterLoad(connection);

	}

	@Override
	public DomainKind getKind() {
		return DomainKind.IndividualResource;
	}

	@Override
	public Key<IndividualResource> getKey() throws PersistenceReferenceException {
		if (getKeyId() == null) {
			throw new PersistenceReferenceException(this.getName() + " has not been saved and cannot therefore be used as a reference");
		}
		if (key == null) {
			key = connection.resourceService()
					.keyFor(this);
		}
		return key;
	}
}
